/**
 * 
 */
package mas;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import com.google.common.base.Optional;

import rinde.sim.core.TimeLapse;
import rinde.sim.core.model.pdp.PDPModel;
import rinde.sim.core.model.road.RoadModel;
/**
 * @author IVAN
 * @description This class implements a Master (leader) truck
 */
public class Master extends Truck{

	/**
	 * @description Each master is assigned an array of slaves and a zone 
	 */
	protected Zone zone;
	protected double SPEED = 5000d; 
	protected int ZoneNumber;
	//Default number of Slaves
	protected List<Box> queue;
	protected List<Slave> slaves;
	private HashMap<BoxMessage,Long> messagesQueued;
	protected List<Box> markedToRemove;
	//TODO: find out what is an appropriate value
	private final Long waitTimeForAcknowledgement = 100000L;

	public Master(int radius, double reliability, int agentNumber, RoadModel rm, PDPModel pm) {
		super(radius,reliability, rm, pm);
		this.slaves = new ArrayList<Slave>();
		this.queue = new ArrayList<Box>();
		this.markedToRemove = new ArrayList<Box>();
		this.agentNumber = agentNumber;
		this.messagesQueued = new HashMap<BoxMessage,Long>();
		}
	
	/**
	 * Adds a new box to the master's queue
	 * @param b
	 */
	public void addToQueue(Box b){
		this.queue.add(b);
	}
	
	/**
	 * Tells if there are still boxes in the queue
	 * @return
	 */
	public boolean hasQueuedBoxes(){
		return (this.queue.size()>0);
	}
	
	/**
	 * Indicates which slave is closest(in time) to a box
	 * @return
	 */
	private boolean getClosestSlave(Box b, TimeLapse time, RoadModel rm){
		boolean assigned = false;		
		long timeToBox = 0;
		Truck agent;
		long current_time = time.getTime();
		Truck selectedAgent = null;
		long minimumTime = Long.MAX_VALUE;
		Iterator<Slave> it = this.slaves.iterator();

		//Make all slaves report time and select minimum
		while(it.hasNext()){// && assigned == false){
			agent = it.next();
			if (!agent.isAtFullCapacity()){
				timeToBox = current_time + agent.reportTime(b); //what time this agent will get the box
					if (timeToBox < minimumTime){
						minimumTime = timeToBox;
						selectedAgent = agent;
						assigned = true;
					}
			}
		}
		//let the master also participate if time to box is smaller
		if (!this.isAtFullCapacity()){
			timeToBox = current_time + this.reportTime(b); //what time this agent will get the box
				if (timeToBox < minimumTime){
					minimumTime = timeToBox;
					selectedAgent = this;		
					
					assigned = true;
				}
		}
		if (assigned && b.getPickupTimeWindow().isAfterStart(minimumTime)){
			if (selectedAgent.getAgentNumber() == this.getAgentNumber())
				this.setBox(b);
			else{
				BoxMessage message = new BoxMessage(this, this.getAndRaiseCounter(), b, selectedAgent);
				this.cm.broadcast(message);
				this.addUnacknowledgedMessage(message, time);
			}
			//selectedAgent.setBox(b);
			return true;
		}
		else{
			return false;
		}
	}
	
	public void addUnacknowledgedMessage(BoxMessage message, TimeLapse time){
		this.messagesQueued.put(message, time.getTime());
	}
	
	public void removeUnacknowledgedMessage(BoxMessage message){
		for(BoxMessage mes: messagesQueued.keySet()){
			if(mes.getBox().getBoxNumber()==message.getBox().getBoxNumber()){
				messagesQueued.remove(mes);
				return;
			}
		}
	}
	
	/**
	 * 
	 * @param box
	 * @return boolean
	 */
	private void assign(TimeLapse time){		
		final RoadModel rm = roadModel.get();
		if(this.hasQueuedBoxes()){			
			//make all slaves report time to next box
			Iterator<Box> boxes_it = this.queue.iterator();
			while(boxes_it.hasNext()){
				Box b = boxes_it.next();						
				if (this.freeAgents()){ //if someone is still not at full capacity
					if(this.getClosestSlave(b,time,rm)){
						this.markToRemove(b);
					}
				}
				else{ //if not, then try to get a new slave from another master
					if(this.askSlave(b, time)){
						this.markToRemove(b);
					}
				}
			}
		}
		this.removeBoxesFromQueue(); //if some boxes were correctly assigned
	}

	/**
	 * If no more slaves search for available slaves in other zones
	 * @param b
	 * @param time
	 * @return
	 */
	private boolean askSlave(Box b, TimeLapse time) {
		boolean assigned = false;		
		long timeToBox = 0;
		Master master;
		long current_time = time.getTime();
		Slave selectedAgent, agent;
		long minimumTime = 999999999;
		
		selectedAgent = null;
		Iterator<Master> it_master = App.masters.iterator();
		while(it_master.hasNext()){ //for every other master
			master = it_master.next();
			if (master.agentNumber != this.agentNumber){
				Iterator<Slave> it_slave = master.getSlaves().iterator();			
				while(it_slave.hasNext()){
					agent = it_slave.next();
					if (!agent.isOccupied()){
						//get closest free slave 
						timeToBox = current_time + agent.reportTime(b); //what time this agent will get the box
							if (timeToBox < minimumTime){
								minimumTime = timeToBox;
								selectedAgent = agent;
								assigned = true;
							}
					}
				}
			}			
		}		
		if (assigned && b.getPickupTimeWindow().isAfterStart(timeToBox)){
			BoxMessage message = new BoxMessage(this, this.getAndRaiseCounter(), b, selectedAgent);
			this.cm.broadcast(message);	
			this.addUnacknowledgedMessage(message,time);
		}
		return assigned;
	}

	/**
	 * Finds out if this master has still slaves not at full capacity
	 * @return
	 */
	protected boolean freeAgents(){
		if (!this.isAtFullCapacity())
			return true;
		if(this.slaves.size()>0){
			Iterator<Slave> it_slaves = this.slaves.iterator();
			while(it_slaves.hasNext()){
				Slave slave = it_slaves.next();
				if (!slave.occupied)
					return true;
				else if (!slave.isAtFullCapacity())
					return true;
					
			}
		}
		return false;
	}
	
	@Override
	protected void tickImpl(TimeLapse time) {	
		this.assign(time);
		super.tickImpl(time);
		Iterator<BoxMessage> it = this.messagesQueued.keySet().iterator();
		ArrayList<BoxMessage> toRemove = new ArrayList<BoxMessage>();
		while(it.hasNext()){
			BoxMessage message = it.next();
			Long timeSinceSending = time.getTime() - messagesQueued.get(message);
			if(timeSinceSending > this.waitTimeForAcknowledgement ){
				if(!App.wasThisBoxDelivered(message.getBox().getBoxNumber())){
					toRemove.add(message);
					//System.out.println(message.getBox().getBoxNumber() + " requeued");
					this.queue.add(message.getBox());
				}
			}
		}
		for(BoxMessage mes: toRemove){
			removeUnacknowledgedMessage(mes);
		}
	}

	@Override
	  public void initRoadPDP(RoadModel pRoadModel, PDPModel pPdpModel) {
	    roadModel = Optional.of(pRoadModel);
	    pdpModel = Optional.of(pPdpModel);
	  }

	public void initializeSlaveLocations(){
		for(Slave slave: slaves){
			slave.setLocation(this.location);
		}
	}

	public void doAction(AcknowledgementMessage message){
		if(!(this.messagesSpread.containsKey(message.getSender()))){
			messagesSpread.put((Truck) message.getSender(), new ArrayList<Integer>());
		}
		ArrayList<Integer> spreadMessagesOfSender = this.messagesSpread.get(message.getSender());
		int counterOfMessage= message.getCounter();
		if(!(spreadMessagesOfSender.contains(counterOfMessage))){
			spreadMessagesOfSender.add(counterOfMessage);
			if(message.getReceiver()==this){
				this.removeUnacknowledgedMessage(message.getBoxMessage());
			}
			else{
				cm.broadcast(message);	
			}	
		}
	}
	
	public Zone getZone() {
		return zone;
	}
	public void setZone(Zone zone) {
		this.zone = zone;
	}
	public int getZoneNumber() {
		return this.getZone().getZoneNumber();
	}
	public List<Slave> getSlaves() {
		return slaves;
	}
	public void setSlaves(List<Slave> slaves) {
		this.slaves = slaves;
	}
	public void registerSlave(Slave slave){
		this.slaves.add(slave);
	}
	public void unregisterSlave(Slave slave){
		this.slaves.remove(slave);
	}
	
	protected void removeBoxesFromQueue() {
		Iterator<Box> it_box= this.markedToRemove.iterator();
		int i;
		while(it_box.hasNext()){
			Box b = it_box.next();
			for(i=0;i<=this.queue.size()-1;i++){
				if (this.queue.get(i).getBoxNumber() == b.getBoxNumber())
				this.queue.remove(i);
			}			
		}
		this.markedToRemove.clear();
	}
	
	@Override
	protected void markToRemove(Box box){
		this.markedToRemove.add(box);
	}

	@Override
	protected void reQueueBox(Box box) {
		this.queue.add(box);
	}	
}
